package com.nulldev.util.io.nfs;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.other.ByteUtils;

public class nFileSystem {
	
	private static final Logger log = LoggerFactory.getLogger(true, true);
	
	public static final long nFS_FILE_HEADER_ID = 0xAAAA;
	public static final long END_OF_HEADERS_MAGIC_LONG = 0xFEE;
	public static final long nFS_FILE_SEPERATOR = 0xAAAAA;
	public static final long nFS_EOF = 0xAAAAE;
	
	/*
	 * nFileSystem, a portable file system allowing compression and encryption
	 * 
	 * Supports:
	 * - nFileVault (Vault folders)
	 * - nFSF - (nFileSystem File - full filesystem in a single file) (IMPLEMENTED)
	 * - Storage in RAM (IMPLEMENTED)
	 * - Storage on a HDD/SSD/USB (IMPLEMENTED)
	 * 
	 * File Structure:
	 * NFS_FILE_HEADER
	 * READ_AHEAD_LONG
	 * [
	 * [ FILE
	 * [ METADATA
	 * [
	 * END_OF_HEADERS_MAGIC
	 * READ_AHEAD_LONG
	 * * file data *
	 * NFS_FILE_SEPERATOR/NFS_EOF
	 * 
	 * Example Metadata:
	 * *THIS WAS SCRAPPED*
	 * {
	 * 	"name": "fileName.txt",
	 * 	"date": "04-21-2018 11:44:21 UTC",
	 * 	"attr": [
	 * 		"FILE_ENCRYPTED",
	 * 		"AES_256"
	 *  ],
	 *  *NEW METHOD*
	 *  
	 *  fileName.txt;04-21-2018@11:44:21-UTC,ENCRYPTED=1,COMPRESSED=0,ENCRYPTION_TYPE=8
	 * }
	 */

	public static enum nFSAttribute {
		NOTHING("", (byte) 0),
		ENCRYPTED("FILE_ENCRYPTED", (byte) 184),
		COMPRESSED("FILE_COMPRESSED", (byte) 175)
		
		;
		
		private final String attrID;
		private final byte headerValue;
		
		private nFSAttribute(final String attrID, final byte INTERNAL_FILE_HEADER) {	
			this.attrID = attrID;
			this.headerValue = INTERNAL_FILE_HEADER;
		}
		
		public String getAttributeID() {
			return this.attrID;
		}
		
		public byte getByteValue() {
			return this.headerValue;
		}
	}
	
	public static enum nFSEncryptionMethod {
		UNENCRYPTED("", (byte) 0),
		AES_128("AES_128", (byte) 4),
		AES_256("AES_256", (byte) 8),
		RSA_2048("RSA_2048", (byte) 5),
		RSA_4096("RSA_4096", (byte) 6),
		XXTEA("XXTEA", (byte) 7),
		eXXTEA("eXXTEA", (byte) 12),
		XOR("XOR", (byte) 9),
		DualXOR("dXOR", (byte) 10),
		DES("DES", (byte) 11)
		
		;
		
		private nFSEncryptionMethod(final String methodID, final byte INTERNAL_FILE_HEADER) {
			
		}
	}
	
	private final HashMap<String, nFile> files = new HashMap<String, nFile>();
	
	public nFileSystem() {
	}
	
	public nFileSystem(final File f) throws IOException {
		if (f == null) {
			throw new NullPointerException();
		}
		final FileInputStream fis = new FileInputStream(f);
		final DataInputStream dis = new DataInputStream(fis);
		final long header = dis.readLong();
		if (header != nFS_FILE_HEADER_ID) {
			throw new UnsupportedOperationException("File is not from nFileSystem! [header=" + header + ",expected=" 
					+ nFS_FILE_HEADER_ID + "]");
		}
		while (true) {
			try {
				final List<Byte> bl = new LinkedList<Byte>();
				final nFile nf = new nFile("NewFile.nf");
				//Step 1: Read metadata
				final long toReadMeta = dis.readLong();
				log.info("Metadata length to read: " + toReadMeta);
				byte b0;
				for (int i = 0; i < toReadMeta; i++) {
					bl.add((b0 = dis.readByte()));
					//JVMUtil.async_println((char) b0);
				}
				dis.readLong(); // skip eof
				nf.setMetadata(ByteUtils.fromList(bl));
				final String[] metaString = new String(nf.getMetadataBytes()).split(";");
				final String name = metaString[0];
				nf.setName(name);
				//Step 2: read data
				bl.clear();
				final long toReadData = dis.readLong();
				log.info("Data length to read: " + toReadData);
				//byte b;
				//long read = 0;
				//for (int i = 0; i < toReadData; i++) {
				//	bl.add((b0 = dis.readByte()));
					//JVMUtil.async_println((char) b0);
				//	read += 1;
				//	JVMUtil.async_println(read + " bytes read.");
				//}
				//nf.setData(ByteUtils.fromList(bl));
				byte[] data = new byte[(int) toReadData];
				dis.read(data);
				nf.setData(data);
				//Step 3: add file/exit
				this.files.put(nf.getName(), nf);
				final long eof = dis.readLong();
				if (eof == nFS_EOF) {
					break;
				}else {
					continue;
				}
			}catch (EOFException eof) {
				break;
			}
		}
		dis.close();
	}
	
	public void addFile(final nFile nf) {
		if (nf == null || nf.getName() == null) {
			throw new NullPointerException();
		}
		this.files.put(nf.getName(), nf);
	}

	public void __print_files() {
		JVMUtil.async_println("Files within this nFileSystem: ");
		ArrayUtils.printLln(files);
	}
	
	public void toFile(final File f) throws IOException {
		this.writeTo(new FileOutputStream(f));
	}
	
	public void writeTo(final OutputStream out) throws IOException {
		if (this.files.isEmpty()) {
			throw new UnsupportedOperationException("This file system is empty!");
		}
		final DataOutputStream dos = new DataOutputStream(out);
		log.info("Creating nFS file...");
		dos.writeLong(nFS_FILE_HEADER_ID);
		dos.flush();
		for (final Map.Entry<String, nFile> entry : this.files.entrySet()) {
			dos.writeLong(entry.getValue().getMetadataBytes().length);
			dos.flush();
			dos.write(entry.getValue().getMetadataBytes());
			dos.flush();
			dos.writeLong(END_OF_HEADERS_MAGIC_LONG);
			dos.flush();
			dos.writeLong(entry.getValue().getData().length);
			dos.flush();
			dos.write(entry.getValue().getData());
			dos.flush();
			if (this.files.size() == 1) {
				dos.writeLong(nFS_EOF);
			}else {
				dos.writeLong(nFS_FILE_SEPERATOR);
			}
			dos.flush();
		}
		dos.flush();
		dos.close();
	}

	public nFile getFile(final String name) {
		return this.files.get(name);
	}
	
	public Set<Entry<String, nFile>> entrySet() {
		return this.files.entrySet();
	}
}
